﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Language.Utility;
using Microsoft.VisualStudio.Imaging;
using Microsoft.VisualStudio.Imaging.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
using MsIntellisense = Microsoft.VisualStudio.Language.Intellisense;

namespace Language.Intellisense.Completion
{
    internal class Source : MsIntellisense.ICompletionSource
    {
        #region Private Types

        private enum AvailableAccess
        {
            Public,
            Protected,
            Private
        }

        private struct MonikerSet
        {
            public ImageMoniker Public { get; set; }
            public ImageMoniker Protected { get; set; }
            public ImageMoniker Private { get; set; }
        }

        private struct CompletionSets
        {
            public IEnumerable<MsIntellisense.Completion> All => Locals.Concat( Parameters ).Concat( Properties ).Concat( Methods ).Concat( Options );
            public ICollection<MsIntellisense.Completion> Locals { get; set; }
            public ICollection<MsIntellisense.Completion> Parameters { get; set; }
            public ICollection<MsIntellisense.Completion> Properties { get; set; }
            public ICollection<MsIntellisense.Completion> Methods { get; set; }
            public ICollection<MsIntellisense.Completion> Options { get; set; }
        }

        #endregion Private Types

        #region Private Data

        private ITextStructureNavigator _navigator;
        private ITextSearchService _searchService;

        private MonikerSet _propertyMonikers = new MonikerSet() { Public = KnownMonikers.PropertyPublic, Protected = KnownMonikers.PropertyProtected, Private = KnownMonikers.PropertyPrivate };
        private MonikerSet _methodMonikers = new MonikerSet() { Public = KnownMonikers.MethodPublic, Protected = KnownMonikers.MethodProtected, Private = KnownMonikers.PropertyPrivate };

        #endregion Private Data

        #region Public Properties

        public Stub.Database Database { get; private set; }
        public ITextBuffer Buffer { get; private set; }

        #endregion Public Properties

        #region C-Tor

        public Source( Stub.Database database, ITextBuffer buffer, ITextStructureNavigatorSelectorService navigatorService, ITextSearchService searchService )
        {
            Database = database;
            Buffer = buffer;

            _navigator = navigatorService.GetTextStructureNavigator( buffer );
            _searchService = searchService;
        }

        #endregion C-Tor

        #region Private Methods

        #region Navigation

        private ITrackingSpan CreateTrackingSpan( SnapshotPoint point, bool mustBeSignificant = true )
        {
            // Are we at the beginning of the file (with nothing left to find)?
            if ( point < 1 )
                return null;

            SnapshotPoint currentPoint = ( point );
            TextExtent extent = _navigator.GetExtentOfWord( currentPoint );

            if ( extent.IsSignificant )
            {
                return currentPoint.Snapshot.CreateTrackingSpan( extent.Span, SpanTrackingMode.EdgeInclusive );
            }
            else
            {
                if ( !mustBeSignificant )
                {
                    return currentPoint.Snapshot.CreateTrackingSpan( currentPoint.Position, 0, SpanTrackingMode.EdgeInclusive );
                }

                // Span is just whitespace, so let's search from the start of the whitespace, see if we can find anything
                return CreateTrackingSpan( extent.Span.Start - 1 );
            }
        }

        private Stub.DetailedContext SpanToStub( ITrackingSpan span )
        {
            string file = span.TextBuffer.ExtractFilename();

            Stub.Token token = Database.FindToken( file, span.GetEndPoint( span.TextBuffer.CurrentSnapshot ), true );

            return Database.ResolveContext( token?.Declaration );
        }

        private ITrackingSpan GetPreviousSpan( SnapshotPoint point, bool significant = true )
        {
            if ( point < 1 )
                return null;

            point = point - 1;

            TextExtent extent = _navigator.GetExtentOfWord( point );

            if ( extent.IsSignificant || !significant )
            {
                return point.Snapshot.CreateTrackingSpan( extent.Span, SpanTrackingMode.EdgeInclusive );
            }
            else
            {
                return GetPreviousSpan( point, significant );
            }
        }

        private ITrackingSpan GetSpanOfWordBeingTyped( SnapshotPoint point )
        {
            ITrackingSpan potential = GetPreviousSpan( point );

            if ( potential == null || potential.GetText( point.Snapshot ).EndsWith( "." ) )
                return point.Snapshot.CreateTrackingSpan( point, 0, SpanTrackingMode.EdgeInclusive );

            return potential;
        }

        private ITrackingSpan GetSpanofParentOfWordBeingTyped( ITrackingSpan parent )
        {
            ITextSnapshot snapshot = parent.TextBuffer.CurrentSnapshot;

            ITrackingSpan potential = GetPreviousSpan( parent.GetStartPoint( snapshot ) );

            if ( potential == null )
                return null;

            string potentialText = potential.GetText( snapshot );

            if ( potentialText == "." )
                return GetPreviousSpan( potential.GetStartPoint( snapshot ) );

            if ( potentialText.EndsWith( ")." ) )
            {
                Span span = potential.GetSpan( snapshot );
                return snapshot.CreateTrackingSpan( span.Start, span.Length - 1, SpanTrackingMode.EdgeInclusive );
            }

            if ( potentialText.EndsWith( "{" ) ||
                 potentialText.EndsWith( "}" ) ||
                 potentialText.EndsWith( ";" ) )
            {
                return null;
            }

            return potential;
        }

        #endregion Navigation

        #region Completion Set Creation

        private void AddToSet( ICollection<MsIntellisense.Completion> completionSet, IEnumerable<Stub.NameContext> contexts, ImageMoniker moniker )
        {
            foreach ( Stub.NameContext context in contexts )
            {
                MsIntellisense.Completion completion = new MsIntellisense.Completion3( context.Name, context.Name, null, moniker, null );
                completionSet.Add( completion );
            }
        }

        private void AddToSet( ICollection<MsIntellisense.Completion> completionSet, IEnumerable<Stub.ConfiguredContext> contexts, AvailableAccess availableAccess, MonikerSet monikerSet )
        {
            foreach ( Stub.ConfiguredContext context in contexts )
            {
                if ( context.Configuration.IsPublic )
                {
                    MsIntellisense.Completion completion = new MsIntellisense.Completion3( context.Name, context.Name, null, monikerSet.Public, null );
                    completionSet.Add( completion );
                }
                else if ( context.Configuration.IsProtected && availableAccess >= AvailableAccess.Protected )
                {
                    MsIntellisense.Completion completion = new MsIntellisense.Completion3( context.Name, context.Name, null, monikerSet.Protected, null );
                    completionSet.Add( completion );
                }
                else if ( context.Configuration.IsPrivate && availableAccess >= AvailableAccess.Private )
                {
                    MsIntellisense.Completion completion = new MsIntellisense.Completion3( context.Name, context.Name, null, monikerSet.Private, null );
                    completionSet.Add( completion );
                }
            }
        }

        private void AddCompletion( CompletionSets sets, Stub.Class stub, AvailableAccess availableAccess )
        {
            AddToSet( sets.Properties, stub.Properties, availableAccess, _propertyMonikers );
            AddToSet( sets.Methods, stub.Functions, availableAccess, _methodMonikers );

            Stub.Class baseClass = Database.ResolveDefinition( stub ) as Stub.Class;

            if ( baseClass != null )
            {
                AvailableAccess baseClassAccess = availableAccess >= AvailableAccess.Protected ? AvailableAccess.Protected : AvailableAccess.Public;

                AddCompletion( sets, baseClass, baseClassAccess );
            }
        }

        private void AddCompletion( CompletionSets sets, Stub.Enum stub )
        {
            AddToSet( sets.Options, stub.Options, KnownMonikers.EnumerationItemPublic );
        }

        private void AddCompletion( CompletionSets sets, Stub.Function stub )
        {
            AddToSet( sets.Locals, stub.Locals, KnownMonikers.LocalVariable );
            AddToSet( sets.Parameters, stub.Parameters, KnownMonikers.Parameter );
        }

        private void ConvertSet( IList<MsIntellisense.CompletionSet> destination, IEnumerable<MsIntellisense.Completion> source, string name, ITrackingSpan span )
        {
            if ( source.Any() )
                destination.Add( new MsIntellisense.CompletionSet( name, name, span, source, null ) );
        }

        private void ConvertSets( IList<MsIntellisense.CompletionSet> destination, CompletionSets source, ITrackingSpan span )
        {
            ConvertSet( destination, source.All, "All", span );
            ConvertSet( destination, source.Locals, "Locals", span );
            ConvertSet( destination, source.Parameters, "Parameters", span );
            ConvertSet( destination, source.Properties, "Properties", span );
            ConvertSet( destination, source.Methods, "Methods", span );
            ConvertSet( destination, source.Options, "Enum", span );
        }

        #endregion Completion Set Creation

        #endregion Private Methods

        #region ICompletionSource

        void MsIntellisense.ICompletionSource.AugmentCompletionSession( MsIntellisense.ICompletionSession session, IList<MsIntellisense.CompletionSet> completionSets )
        {
            SnapshotPoint caretPosition = session.TextView.Caret.Position.BufferPosition;

            ITrackingSpan spanOfWordBeingTyped = GetSpanOfWordBeingTyped( caretPosition );
            ITrackingSpan spanOfParentWord = GetSpanofParentOfWordBeingTyped( spanOfWordBeingTyped );

            CompletionSets sets = new CompletionSets()
            {
                Locals = new List<MsIntellisense.Completion>(),
                Parameters = new List<MsIntellisense.Completion>(),
                Properties = new List<MsIntellisense.Completion>(),
                Methods = new List<MsIntellisense.Completion>(),
                Options = new List<MsIntellisense.Completion>()
            };

            if ( spanOfParentWord == null )
            {
                string path = spanOfWordBeingTyped.TextBuffer.ExtractFilename();
                Stub.File file = Database.FindFile( path );

                if( file != null )
                {
                    IEnumerable<Stub.IScope> scopes = file.FindScopes( spanOfWordBeingTyped.GetStartPoint( spanOfWordBeingTyped.TextBuffer.CurrentSnapshot ) );

                    foreach ( var scope in scopes )
                    {
                        Stub.Class classStub = null;
                        Stub.Function funcStub = null;

                        if ( ( classStub = scope as Stub.Class ) != null )
                        {
                            AddCompletion( sets, classStub, AvailableAccess.Private );
                        }
                        else if ( ( funcStub = scope as Stub.Function ) != null )
                        {
                            AddCompletion( sets, funcStub );
                        }
                    }
                }
            }
            else
            {
                Stub.DetailedContext spanContext = SpanToStub( spanOfParentWord );
                Stub.DetailedContext typeContext = Database.ResolveDefinition( spanContext as Stub.TypedContext );

                Stub.Class classStub = null;
                Stub.Enum enumStub = null;

                if ( ( classStub = typeContext as Stub.Class ) != null )
                {
                    AddCompletion( sets, classStub, AvailableAccess.Public );
                }
                else if ( ( enumStub = spanContext as Stub.Enum ) != null )
                {
                    AddCompletion( sets, enumStub );
                }
            }

            if( completionSets.Any() )
                session.SelectedCompletionSet = completionSets.First();
            ConvertSets( completionSets, sets, spanOfWordBeingTyped );
        }

        #endregion ICompletionSource

        #region IDisposable

        private bool _disposedValue = false; // To detect redundant calls

        protected virtual void Dispose( bool disposing )
        {
            if ( !_disposedValue )
            {
                if ( disposing )
                {
                    // TODO: dispose managed state (managed objects).
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                _disposedValue = true;
            }
        }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        // ~TestCompletionSource() {
        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        //   Dispose(false);
        // }

        // This code added to correctly implement the disposable pattern.
        void IDisposable.Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose( true );
            // TODO: uncomment the following line if the finalizer is overridden above.
            // GC.SuppressFinalize(this);
        }

        #endregion IDisposable
    }

    #region Provider

    [Export( typeof( MsIntellisense.ICompletionSourceProvider ) )]
    [ContentType( "redscript" )]
    [Name( "Token Completion" )]
    internal class SourceProvider : MsIntellisense.ICompletionSourceProvider
    {
        #region Properties

        [Import]
        internal ITextStructureNavigatorSelectorService NavigatorService { get; set; }

        [Import]
        internal ITextSearchService TextSearchService { get; set; }

        [Import]
        public Stub.Database Database { get; set; }

        #endregion Properties

        #region ICompletionSourceProvider

        MsIntellisense.ICompletionSource MsIntellisense.ICompletionSourceProvider.TryCreateCompletionSource( ITextBuffer textBuffer )
        {
            return new Source( Database, textBuffer, NavigatorService, TextSearchService );
        }

        #endregion ICompletionSourceProvider
    }

    #endregion Provider
}
